/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package org.eclipse.ceylon.ide.eclipse.core.classpath;

import static org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder.getCeylonClassesOutputFolder;
import static org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder.isExplodeModulesEnabled;
import static org.eclipse.ceylon.ide.eclipse.core.classpath.CeylonClasspathUtil.ceylonSourceArchiveToJavaSourceArchive;
import static org.eclipse.ceylon.ide.eclipse.java2ceylon.Java2CeylonProxies.modelJ2C;
import static org.eclipse.ceylon.ide.eclipse.java2ceylon.Java2CeylonProxies.utilJ2C;
import static org.eclipse.ceylon.ide.eclipse.ui.CeylonPlugin.PLUGIN_ID;
import static org.eclipse.ceylon.ide.eclipse.util.InteropUtils.toJavaString;
import static java.util.Arrays.asList;
import static java.util.Collections.synchronizedSet;
import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace;
import static org.eclipse.jdt.core.JavaCore.newLibraryEntry;
import static org.eclipse.jdt.core.JavaCore.setClasspathContainer;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.DeltaProcessingState;
import org.eclipse.jdt.internal.core.JavaElementDelta;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.ui.packageview.PackageExplorerContentProvider;
import org.eclipse.jdt.internal.ui.util.CoreUtility;

import org.eclipse.ceylon.cmr.api.ArtifactContext;
import org.eclipse.ceylon.cmr.api.RepositoryManager;
import org.eclipse.ceylon.cmr.impl.MavenRepository;
import org.eclipse.ceylon.common.ModuleSpec;
import org.eclipse.ceylon.compiler.typechecker.TypeChecker;
import org.eclipse.ceylon.compiler.typechecker.context.Context;
import org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder;
import org.eclipse.ceylon.ide.eclipse.core.model.LookupEnvironmentUtilities;
import org.eclipse.ceylon.ide.common.model.BaseIdeModule;
import org.eclipse.ceylon.ide.common.model.CeylonIdeConfig;
import org.eclipse.ceylon.ide.common.model.CeylonProject;
import org.eclipse.ceylon.ide.common.model.CeylonProjectConfig;
import org.eclipse.ceylon.ide.common.platform.platformUtils_;
import org.eclipse.ceylon.ide.common.util.ProgressMonitor;
import org.eclipse.ceylon.ide.common.util.ProgressMonitor$impl;
import org.eclipse.ceylon.ide.common.util.ProgressMonitorChild;
import org.eclipse.ceylon.model.cmr.ArtifactResultType;
import org.eclipse.ceylon.model.typechecker.model.Module;

/**
 * Eclipse classpath container that will contain the Ceylon resolved entries.
 */
public class CeylonProjectModulesContainer implements IClasspathContainer {

    public static final String CONTAINER_ID = PLUGIN_ID + ".cpcontainer.CEYLON_CONTAINER";

    private IClasspathEntry[] classpathEntries;
    private IPath path;
    //private String jdtVersion;
    private IJavaProject javaProject;
    
    private Set<String> modulesWithSourcesAlreadySearched = synchronizedSet(new HashSet<String>());

    public IJavaProject getJavaProject() {
        return javaProject;
    }

    public IClasspathAttribute[] getAttributes() {
        return attributes;
    }

    /**
     * attributes attached to the container but not Ceylon related (Webtools or AspectJfor instance)
     */
    private IClasspathAttribute[] attributes = new IClasspathAttribute[0];

    public CeylonProjectModulesContainer(IJavaProject javaProject, IPath path,
            IClasspathEntry[] classpathEntries, IClasspathAttribute[] attributes) {
        this.path = path;
        this.attributes = attributes; 
        this.classpathEntries = classpathEntries;
        this.javaProject = javaProject;
    }

    public CeylonProjectModulesContainer(IProject project) {
        javaProject = JavaCore.create(project);
        path = new Path(CeylonProjectModulesContainer.CONTAINER_ID + "/default");
        classpathEntries = new IClasspathEntry[0];
        attributes = new IClasspathAttribute[0];
    }
    
    public CeylonProjectModulesContainer(CeylonProjectModulesContainer cp) {
        path = cp.path;
        javaProject = cp.javaProject;        
        classpathEntries = cp.classpathEntries;
        attributes = cp.attributes;
        modulesWithSourcesAlreadySearched = cp.modulesWithSourcesAlreadySearched;
    }

    public String getDescription() {
        return "Ceylon Project Modules";
    }

    public int getKind() {
        return K_APPLICATION;
    }

    public IPath getPath() {
        return path;
    }

    public IClasspathEntry[] getClasspathEntries() {
        return classpathEntries;
    }

    public IClasspathEntry addNewClasspathEntryIfNecessary(IPath modulePath) {
        synchronized (classpathEntries) {
            for (IClasspathEntry cpEntry : classpathEntries) {
                if (cpEntry.getPath().equals(modulePath)) {
                    return null;
                }
            }
            IClasspathEntry newEntry = newLibraryEntry(modulePath, null, null);
            IClasspathEntry[] newClasspathEntries = new IClasspathEntry[classpathEntries.length + 1];
            if (classpathEntries.length > 0) {
                System.arraycopy(classpathEntries, 0, newClasspathEntries, 0, classpathEntries.length);
            }
            newClasspathEntries[classpathEntries.length] = newEntry;
            classpathEntries = newClasspathEntries;
            return newEntry;
        }
    }
    
    /*private static final ISchedulingRule RESOLVE_EVENT_RULE = new ISchedulingRule() {
        public boolean contains(ISchedulingRule rule) {
            return rule == this;
        }

        public boolean isConflicting(ISchedulingRule rule) {
            return rule == this;
        }
    };*/

    public void runReconfigure() {
        modulesWithSourcesAlreadySearched.clear();
        Job job = new Job("Resolving dependencies for project " + 
                getJavaProject().getElementName()) {
            @Override 
            protected IStatus run(IProgressMonitor monitor) {
                final IProject project = javaProject.getProject();
                ProgressMonitor$impl<IProgressMonitor>.Progress progress = 
                        utilJ2C().wrapProgressMonitor(monitor)
                            .Progress$new$(1000, 
                                    ceylon.language.String.instance("Resolving classpath of project " + project.getName()));
                try {
                    
                    final IClasspathEntry[] classpath = constructModifiedClasspath(javaProject);                    
                    javaProject.setRawClasspath(classpath, monitor);
                    
                    boolean changed = resolveClasspath(progress.newChild(800), false);
                    if(changed) {
                        refreshClasspathContainer(progress.newChild(200));
                    }
                    
                    // Rebuild the project :
                    //   - without referenced projects
                    //   - with referencing projects
                    //   - and force the rebuild even if the model is already typechecked
                    
                    Job job = new BuildProjectAfterClasspathChangeJob("Rebuild of project " + 
                            project.getName(), project, false, true, true);
                    job.setRule(project.getWorkspace().getRoot());
                    job.schedule(3000);
                    job.setPriority(Job.BUILD);
                    return Status.OK_STATUS;
                    
                } 
                catch (CoreException e) {
                    e.printStackTrace();
                    return new Status(IStatus.ERROR, PLUGIN_ID,
                            "could not resolve dependencies", e);
                }
                finally {
                    progress.destroy(null);
                }
            }            
        };
        job.setUser(false);
        job.setPriority(Job.BUILD);
        job.setRule(getWorkspace().getRoot());
        job.schedule();
    }
    
    private IClasspathEntry[] constructModifiedClasspath(IJavaProject javaProject) 
            throws JavaModelException {
        IClasspathEntry newEntry = JavaCore.newContainerEntry(path, null, 
                new IClasspathAttribute[0], true);
        IClasspathEntry[] entries = javaProject.getRawClasspath();
        List<IClasspathEntry> newEntries = new ArrayList<IClasspathEntry>(asList(entries));
        int index = 0;
        boolean mustReplace = false;
        boolean projectModulesEntryWasExported = false;
        for (IClasspathEntry entry: newEntries) {
            if (entry.getPath().equals(newEntry.getPath()) ) {
                mustReplace = true;
                projectModulesEntryWasExported = entry.isExported();
                break;
            }
            index++;
        }

        newEntry = JavaCore.newContainerEntry(path, null, 
                new IClasspathAttribute[0], !mustReplace || projectModulesEntryWasExported);
        if (mustReplace) {
            newEntries.set(index, newEntry);
        }
        else {
            newEntries.add(newEntry);
        }
        return (IClasspathEntry[]) newEntries.toArray(new IClasspathEntry[newEntries.size()]);
    }

    void notifyUpdateClasspathEntries() {
        // Changes to resolved classpath are not announced by JDT Core
        // and so PackageExplorer does not properly refresh when we update
        // the classpath container.
        // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=154071
        DeltaProcessingState s = JavaModelManager.getJavaModelManager().deltaState;
        synchronized (s) {
            IElementChangedListener[] listeners = s.elementChangedListeners;
            for (int i = 0; i < listeners.length; i++) {
                if (listeners[i] instanceof PackageExplorerContentProvider) {
                    JavaElementDelta delta = new JavaElementDelta(javaProject);
                    delta.changed(IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED);
                    listeners[i].elementChanged(new ElementChangedEvent(delta,
                            ElementChangedEvent.POST_CHANGE));
                }
            }
        }
        //I've disabled this because I don't really like having it, but
        //it does seem to help with the issue of archives appearing
        //empty in the package manager
        /*try {
            javaProject.getProject().refreshLocal(IResource.DEPTH_ONE, null);
        } 
        catch (CoreException e) {
            e.printStackTrace();
        }*/
    }

    /**
     * Resolves the classpath entries for this container.
     * @param monitor
     * @param reparse
     * @return true if the classpath was changed, false otherwise.
     */
    public boolean resolveClasspath(ProgressMonitor<IProgressMonitor> mon, boolean reparse)  {
        IJavaProject javaProject = getJavaProject();
        IProject project = javaProject.getProject();
        ProgressMonitor$impl<IProgressMonitor>.Progress ceylonMonitor = mon.Progress$new$(1000, 
                    ceylon.language.String.instance("Resolving classpath of project " + project.getName()));
        CeylonProject<IProject, IResource, IFolder, IFile> ceylonProject = modelJ2C().ceylonModel().getProject(project);
        
        try {

            TypeChecker typeChecker = null;
            if (!reparse) {
                typeChecker = ceylonProject.getTypechecker();
            }
            IClasspathEntry[] oldEntries = classpathEntries;
            if (typeChecker==null) {
                IClasspathEntry explodeFolderEntry = null;
                if (oldEntries != null) {
                    for (IClasspathEntry entry : oldEntries) {
                        if (entry.getPath() != null && entry.getPath().equals(getCeylonClassesOutputFolder(project).getFullPath())) {
                            explodeFolderEntry = entry;
                            break;
                        }
                    }
                }
                
                IClasspathEntry[] resetEntries = explodeFolderEntry == null ? 
                        new IClasspathEntry[] {} : 
                            new IClasspathEntry[] {explodeFolderEntry};
                
                JavaCore.setClasspathContainer(getPath(), 
                        new IJavaProject[]{javaProject}, 
                        new IClasspathContainer[]{ new CeylonProjectModulesContainer(javaProject, getPath(), resetEntries, attributes)} , ceylonMonitor.newChild(100).getWrapped());
                ceylonProject.parseCeylonModel(ceylonMonitor.newChild(800));
            }
            
            typeChecker = ceylonProject.getTypechecker();

            IFolder explodedModulesFolder = getCeylonClassesOutputFolder(project);
            if (isExplodeModulesEnabled(project)) {
                if (!explodedModulesFolder.exists()) {
                    CoreUtility.createDerivedFolder(explodedModulesFolder, true, true, ceylonMonitor.newChild(10).getWrapped());
                } else {
                    if (!explodedModulesFolder.isDerived()) {
                        explodedModulesFolder.setDerived(true, ceylonMonitor.newChild(10).getWrapped());
                    }
                }
            }
            else {
                if (explodedModulesFolder.exists()) {
                    explodedModulesFolder.delete(true, ceylonMonitor.newChild(10).getWrapped());
                }
            }

            final Collection<IClasspathEntry> paths = findModuleArchivePaths(
                    javaProject, project, typeChecker);

            CeylonProjectModulesContainer currentContainer = (CeylonProjectModulesContainer) JavaCore.getClasspathContainer(path, javaProject);
            if (oldEntries == null || 
                    oldEntries != currentContainer.classpathEntries ||
                    !paths.equals(asList(oldEntries))) {
                this.classpathEntries = paths.toArray(new IClasspathEntry[paths.size()]);
                return true;
            }
        }
        catch (CoreException e) {
            e.printStackTrace();
        }
        finally {
            ceylonMonitor.destroy(null);
        }
        return false;
        
    }

    public void refreshClasspathContainer(ProgressMonitorChild<IProgressMonitor> monitor) throws JavaModelException {
        IJavaProject javaProject = getJavaProject();
        setClasspathContainer(path, new IJavaProject[] { javaProject },
                new IClasspathContainer[] {new CeylonProjectModulesContainer(this)}, monitor.getWrapped());
        LookupEnvironmentUtilities.Provider modelLoader = (LookupEnvironmentUtilities.Provider) CeylonBuilder.getProjectModelLoader(javaProject.getProject());
        if (modelLoader != null) {
            modelLoader.refreshNameEnvironment();
        }
        //update the package manager UI
        new Job("update package manager") {
            @Override
            protected IStatus run(IProgressMonitor monitor) {
                notifyUpdateClasspathEntries();
                return Status.OK_STATUS;
            }
        }.schedule();
    }

    private Collection<IClasspathEntry> findModuleArchivePaths(
            IJavaProject javaProject, IProject project, TypeChecker typeChecker) 
                    throws JavaModelException, CoreException {
        final Map<String, IClasspathEntry> paths = new TreeMap<String, IClasspathEntry>();

        ModuleSpec jdkProviderSpec = null;
        CeylonProjectConfig ceylonConfig = modelJ2C().ceylonConfig(project);
        if (ceylonConfig != null) {
            ceylon.language.String jdkProviderString = ceylonConfig.getJdkProvider();
            if (jdkProviderString != null) {
            	jdkProviderSpec = ModuleSpec.parse(jdkProviderString.toString());
            }
        }
        
        Context context = typeChecker.getContext();
        RepositoryManager provider = context.getRepositoryManager();
        Set<Module> modulesToAdd = context.getModules().getListOfModules();
        //modulesToAdd.add(projectModules.getLanguageModule());        
        for (Module module: modulesToAdd) {
            BaseIdeModule jdtModule = (BaseIdeModule) module;
            String name = module.getNameAsString(); 
            if (name.equals(Module.DEFAULT_MODULE_NAME) ||
                    !(jdtModule.getIsJavaBinaryArchive() || jdtModule.getIsCeylonArchive()) ||
                    module.equals(module.getLanguageModule()) ||
                    ! module.isAvailable()) {
                continue;
            }
            if (jdkProviderSpec != null &&
            		jdkProviderSpec.getName().equals(module.getNameAsString())) {
            	continue;
            }
            
            IPath modulePath = getModuleArchive(provider, jdtModule);
            if (modulePath!=null) {
                IPath srcPath = null;
                
                for (IProject p: project.getReferencedProjects()) {
                    if (p.isAccessible()
                            && p.getLocation().isPrefixOf(modulePath)) {
                        //the module belongs to a referenced
                        //project, so use the project source
                        srcPath = p.getLocation();
                        break;
                    }
                }
                
                if (srcPath==null) {
                    for (IClasspathEntry entry : classpathEntries) {
                        if (entry.getPath().equals(modulePath)) {
                            srcPath = entry.getSourceAttachmentPath();
                            break;
                        }
                    }
                }

                if (srcPath==null && 
                        !modulesWithSourcesAlreadySearched.contains(module.toString())) {
                    //otherwise, use the src archive
                    srcPath = getSourceArchive(provider, jdtModule);
                    if ((srcPath == null || srcPath.equals(modulePath))
                            && jdtModule.getIsJavaBinaryArchive()) {
                        CeylonIdeConfig ideConfig = modelJ2C().ideConfig(project);
                        if (ideConfig != null) {
                            ceylon.language.String attachment = 
                                    ideConfig.getSourceAttachment(
                                            jdtModule.getNameAsString(), jdtModule.getVersion());
                            if (attachment != null) {
                                String a = attachment.toString();
                                srcPath=new Path(a);
                                if (! srcPath.isAbsolute()) {
                                    if (a.startsWith("../") || 
                                            a.startsWith("./")) {
                                        srcPath = project.getLocation().append(srcPath);
                                    } else {
                                        srcPath = project.getFullPath().append(srcPath);
                                    }
                                }
                            }
                        }
                    }
                }
                modulesWithSourcesAlreadySearched.add(module.toString());
                IClasspathEntry newEntry = newLibraryEntry(modulePath, srcPath, null);
                paths.put(newEntry.toString(), newEntry);

            }
            else {
                // FIXME: ideally we should find the module.java file and put the marker there, but
                // I've no idea how to find it and which import is the cause of the import problem
                // as it could be transitive
                IMarker marker = project.createMarker(IJavaModelMarker.BUILDPATH_PROBLEM_MARKER);
                marker.setAttribute(IMarker.MESSAGE, "no module archive found for classpath container: " + 
                        module.getNameAsString() + "/" + module.getVersion());
                marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
                marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
            }
        }

        if (isExplodeModulesEnabled(project)) {
            IClasspathEntry newEntry = newLibraryEntry(getCeylonClassesOutputFolder(project).getFullPath(), 
                    null, null, false);
            paths.put(newEntry.toString(), newEntry);
        }
        
        return asList(paths.values().toArray(new IClasspathEntry[paths.size()]));
    }

    public static File getSourceArtifact(RepositoryManager provider,
            BaseIdeModule module) {
        String sourceArchivePath = toJavaString(module.getSourceArchivePath()); 
        if (sourceArchivePath != null) {
            File sourceArchive = new File(sourceArchivePath);
            if (sourceArchive.exists()) {
                return sourceArchive; 
            }
        }
        
        // BEWARE : here the request to the provider is done in 2 steps, because if
        // we do this in a single step, the Aether repo might return the .jar
        // archive as a default result when not finding it with the .src extension.
        // In this case it will not try the second extension (-sources.jar).
        String suffix = module.getArtifactType().equals(ArtifactResultType.MAVEN) ? 
                ArtifactContext.LEGACY_SRC : ArtifactContext.SRC; 
        String namespace = module.getArtifactType().equals(ArtifactResultType.MAVEN) ? 
                MavenRepository.NAMESPACE : null; 
        ArtifactContext ctx = new ArtifactContext(namespace, module.getNameAsString(), 
                module.getVersion(), suffix);
        File srcArtifact = null;
        try {
            srcArtifact = provider.getArtifact(ctx);
        } catch(Exception e) {
            if (e.getCause() != null && e.getCause() instanceof Exception) {
                e = (Exception) e.getCause();
            }
            platformUtils_.get_().log(org.eclipse.ceylon.ide.common.platform.Status.getStatus$_WARNING(), "Eexception during retrieval of the artifact " + ctx + " : ", e);
        }
        if (srcArtifact!=null) {
            if (srcArtifact.getPath().endsWith(suffix)) {
                return srcArtifact;
            }
        }
        return null;
    }

    public static IPath getSourceArchive(RepositoryManager provider,
            BaseIdeModule module) {
        File srcArtifact = getSourceArtifact(provider, module);
        if (srcArtifact!=null) {
            if (module.getIsCeylonBinaryArchive()) {
                if (module.containsJavaImplementations()) {
                    srcArtifact = ceylonSourceArchiveToJavaSourceArchive(
                            module.getNameAsString(),
                            module.getVersion(),
                            srcArtifact);
                } else {
                    srcArtifact = null;
                }
            }
        }
        
        if (srcArtifact!=null) {
            return new Path(srcArtifact.getAbsolutePath());
        }
        
        return null;
    }

    public static File getModuleArtifact(RepositoryManager provider,
            BaseIdeModule module) {
        if (! module.getIsSourceArchive()) {
            File moduleFile = module.getArtifact();
            if (moduleFile == null) {
                return null;
            }
            if (moduleFile.exists()) {
                return moduleFile;
            }
        }
        // Shouldn't need to execute this anymore ! 
        // We already retrieved this information during in the ModuleVisitor.
        // This should be a performance gain.
        ArtifactContext ctx = new ArtifactContext(null, module.getNameAsString(), 
                module.getVersion(), ArtifactContext.CAR);
        // try first with .car
        File moduleArtifact = null;
        try {
            moduleArtifact = provider.getArtifact(ctx);
        } catch(Exception e) {
            if (e.getCause() != null && e.getCause() instanceof Exception) {
                e = (Exception) e.getCause();
            }
            platformUtils_.get_().log(org.eclipse.ceylon.ide.common.platform.Status.getStatus$_WARNING(), "Exception during retrieval of the artifact " + ctx + " : ", e);
        }
        if (moduleArtifact==null){
            // try with .jar
            ctx = new ArtifactContext(null, module.getNameAsString(), 
                    module.getVersion(), ArtifactContext.JAR);
            moduleArtifact = provider.getArtifact(ctx);
        }
        return moduleArtifact;
    }

    public static IPath getModuleArchive(RepositoryManager provider,
            BaseIdeModule module) {
        File moduleArtifact = getModuleArtifact(provider, module);
        if (moduleArtifact!=null) {
            return new Path(moduleArtifact.getPath());
        }
        return null;
    }

    public static boolean isProjectModule(IJavaProject javaProject, Module module)
            throws JavaModelException {
        boolean isSource=false;
        for (IPackageFragmentRoot s: javaProject.getPackageFragmentRoots()) {
            if (s.exists() 
                    && javaProject.isOnClasspath(s) 
                    && s.getKind()==IPackageFragmentRoot.K_SOURCE 
                    && s.getPackageFragment(module.getNameAsString()).exists()) {
                isSource=true;
                break;
            }
        }
        return isSource;
    }
}
